Skip to content

[中文版本](/posts/c教程/zh-CN/09-可移植与现代 C/083-Inline Assembly and Hardware Access)

C gives you precise control over memory and performance, but sometimes you need to go one level deeper, directly to the CPU. That’s where inline assembly comes in: embedding assembly language inside your C code to optimize performance or access hardware-level features.

This chapter will show how to mix C and assembly safely, portably, and meaningfully.

Step 1. What Is Inline Assembly? ​

Inline assembly lets you insert small snippets of machine instructions into your C program. You can use it to:

  • Access CPU instructions not exposed by C.
  • Optimize performance-critical paths.
  • Implement hardware drivers or low-level routines.

However, it’s also non-portable and compiler-specific, so use it sparingly and isolate it behind clean C interfaces.

Step 2. Two Common Flavors ​

GCC / Clang syntax (AT&T or Intel style) Uses theasm or__asm__ keyword.

MSVC syntax Uses__asm { ... } inside functions.

We’ll focus on GCC/Clang syntax, since it’s used in most systems programming contexts.

Step 3. Basic Inline Assembly Example ​

Tiny Code: Print CPU ID register (x86 only)

#include <stdio.h>
int main(void) {
    unsigned int eax, ebx, ecx, edx;
    eax = 0;
    __asm__ __volatile__(
        "cpuid"
        : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx)
        : "a"(0)
    );
    printf("CPU Vendor: %.4s%.4s%.4s\n",
           (char*)&ebx, (char*)&edx, (char*)&ecx);
}

Explanation:

  • cpuid is a CPU instruction that fills registers with information.
  • "=a"(eax) means “store the output of registereax into variableeax.”
  • : "a"(0) means “put 0 intoeax before running the instruction.”
  • The__volatile__ keyword tells the compiler not to optimize it away.

Step 4. GCC Inline Assembly Syntax ​

General form:

asm volatile ("instruction list"
              : output_operands
              : input_operands
              : clobbered_registers);

Example:

asm volatile ("addl %%ebx, %%eax"
              : "=a"(result)
              : "a"(x), "b"(y));

Explanation:

  • "addl %%ebx, %%eax", assembly instruction
  • "=a"(result), output ineax goes toresult
  • "a"(x), "b"(y), inputs: putx ineax,y inebx

Step 5. Reading CPU Cycle Counters ​

Tiny Code: Measure CPU cycles between operations

#include <stdio.h>
unsigned long long rdtsc(void) {
    unsigned int lo, hi;
    __asm__ __volatile__("rdtsc" : "=a"(lo), "=d"(hi));
    return ((unsigned long long)hi << 32) | lo;
}
int main(void) {
    unsigned long long start = rdtsc();
    for (volatile int i = 0; i < 1000000; i++);
    unsigned long long end = rdtsc();
    printf("Cycles: %llu\n", end - start);
}

Explanation:

  • rdtsc reads the CPU’s timestamp counter.
  • It’s a precise measure of time in CPU cycles, great for microbenchmarking.

Step 6. Writing to I/O Ports (Embedded or Kernel Context) ​

If you’re writing embedded code or OS kernels, you often interact with hardware registers directly.

Example (x86, privileged mode only):

static inline void outb(unsigned short port, unsigned char value) {
    __asm__ __volatile__("outb %0, %1" : : "a"(value), "Nd"(port));
}

This writes a byte to an I/O port, used for devices like serial ports, timers, or PIC controllers.

In user-space, you generally can’t do this (needs kernel privileges).

Step 7. Memory Barriers and CPU Fences ​

When working with concurrency or hardware, you may need to control instruction ordering.

__asm__ __volatile__("mfence" ::: "memory");

This tells the CPU and compiler not to reorder memory operations, essential for writing thread-safe or device-control code at the hardware level.

Step 8. Register Constraints ​

GCC lets you specify which registers to use.

ConstraintRegisterMeaning
"a"eaxaccumulator
"b"ebxbase
"c"ecxcounter
"d"edxdata
"S"esisource index
"D"edidestination index

Example:

asm("mul %1" : "=a"(res) : "r"(x));

The"r" constraint lets the compiler choose any register.

Step 9. Mixing Assembly and C Functions ​

You can write small routines in separate.S files (pure assembly) and call them from C:

# file: add.S
.global add_two
add_two:
    addl %esi, %edi
    movl %edi, %eax
    ret

Then in C:

int add_two(int a, int b);
int main(void) {
    printf("%d\n", add_two(5, 7));
}

This hybrid style is used in OS kernels, bootloaders, and math libraries.

Step 10. Tiny Code: Inline Assembly Add Function ​

#include <stdio.h>
int add_fast(int a, int b) {
    int result;
    __asm__ ("addl %1, %0" : "=r"(result) : "r"(b), "0"(a));
    return result;
}
int main(void) {
    printf("%d\n", add_fast(3, 5));
}

The"0"(a) constraint tells the compiler to use the same register for input and output.

Why It Matters ​

Inline assembly teaches you what really happens beneath your C code. Even if you rarely use it, understanding it helps you:

  • Read compiler-generated assembly (gcc -S)
  • Optimize performance-critical code
  • Understand how system calls, context switches, and kernel traps work

It’s where software meets hardware, the true metal of computing.

Try It Yourself ​

  1. Write a small inline assembly snippet that swaps two integers.
  2. Print the CPU vendor string withcpuid.
  3. Userdtsc() to benchmark your function.
  4. Inspect compiler-generated assembly usinggcc -S.
  5. Try to reimplement a basic math operation in assembly and compare performance.

Next, you’ll learn cross-compilation, how to build your C programs for other architectures and systems, from your own machine.

本站总访问 — 次